package com.sync.data.common.util;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;

import com.sync.data.common.annotation.sign.Join;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.jpa.domain.Specification;

import javax.persistence.criteria.*;
import com.sync.data.common.annotation.sign.*;
import java.lang.reflect.Field;
import java.util.*;
import java.util.function.Consumer;

public final class SpecificationBuilder {

    private static final Logger LOGGER = LoggerFactory.getLogger(SpecificationBuilder.class);

    private static final String PERCENT = "%";

    private static final int MAX_IN_SIZE = 1000;

    private static final String SERIAL_VERSION_UID = "serialVersionUID";

    private SpecificationBuilder() {
    }

    public static <T> Specification<T> buildSpecification(Object condition) {
        Class<?> conditionClass = condition.getClass();
        List<Field> declaredFields = ReflectionUtil.getFields(conditionClass);
        final Map<String, List<Predicate>> orPredicateMap = new HashMap<>();
        final Map<String, List<Predicate>> andPredicateMap = new HashMap<>();
        final Map<String, javax.persistence.criteria.Join> joinMap = new HashMap<>();
        return (root, query, cb) -> {
            query.distinct(true);

            try {
                dealGroupBy(query,root,condition);
                BeanUtil.setProperty(condition,"groupBy",null);
            }catch (Exception e){

            }

            List<Predicate> predicates = new ArrayList<>(declaredFields.size());
            List<Predicate> predicatesGroupAnd = new ArrayList<>();
            Nullable.of(declaredFields).ifPresent(fields -> fields.stream().filter(checkFiledAccess()).forEach(fieldParse(root, predicates, cb, condition, orPredicateMap, andPredicateMap, joinMap)));
            if (CollUtil.isNotEmpty(orPredicateMap)) {
                for (List<Predicate> subOrPredicates : orPredicateMap.values()) {
                    predicates.add(cb.or(subOrPredicates.toArray(new Predicate[subOrPredicates.size()])));
                }
            }

            if (CollUtil.isNotEmpty(andPredicateMap)) {
                for (List<Predicate> subAndPredicates : andPredicateMap.values()) {
                    predicatesGroupAnd.add(cb.and(subAndPredicates.toArray(new Predicate[subAndPredicates.size()])));
                }
                predicates.add(cb.or(predicatesGroupAnd.toArray(new Predicate[predicatesGroupAnd.size()])));
            }
            return cb.and(predicates.toArray(new Predicate[predicates.size()]));
        };
    }

    private static void dealGroupBy(CriteriaQuery query, Path root, Object condition){

        Object groupBy = BeanUtil.getProperty(condition, "groupBy");

        if(ObjectUtil.isNotNull(groupBy)){
            List<String> groupByList = (List<String>) groupBy;
            if(CollectionUtil.isNotEmpty(groupByList)){
                List<Expression<?>> grouping = new ArrayList<>();
                for(String attributeName : groupByList){
                    grouping.add(root.get(attributeName));
                }
                query.groupBy(grouping);
            }
        }
    }

    private static java.util.function.Predicate<Field> checkFiledAccess() {
        return field -> !(field.isAnnotationPresent(Page.class) || field.isAnnotationPresent(Ignore.class) || SERIAL_VERSION_UID.equals(field.getName()));
    }

    private static <T> Consumer<Field> fieldParse(Root root, List<Predicate> predicates, CriteriaBuilder cb, T condition, Map<String, List<Predicate>> orPredicateMap, Map<String, List<Predicate>> andPredicateMap, Map<String, javax.persistence.criteria.Join> joinMap) {
        return field -> {
            try {
                field.setAccessible(true);
                //优先从Name注解上取自定义名称
                String fieldName = field.isAnnotationPresent(Name.class) ? field.getAnnotation(Name.class).value() : field.getName();
                Nullable.of(field.get(condition)).ifPresent(fieldObject -> appendPredicate(root, predicates, field, fieldName, fieldObject, cb, orPredicateMap, andPredicateMap, joinMap));
            } catch (IllegalAccessException e) {
                LOGGER.error("SpecificationBuilder buildSpecification fieldParse error", e);
            }
        };
    }

    private static void appendPredicate(Root root, List<Predicate> predicates, Field field, String fieldName, Object fieldObject, CriteriaBuilder cb, Map<String, List<Predicate>> orPredicateMap, Map<String, List<Predicate>> andPredicateMap, Map<String, javax.persistence.criteria.Join> joinMap) {
        //查看是否有group属性，如果有，则需要进行分类or查询
        if (field.isAnnotationPresent(GroupOr.class)) {
            GroupOr groupOr = field.getAnnotation(GroupOr.class);
            List<Predicate> predicates1 = orPredicateMap.getOrDefault(groupOr.value(), new ArrayList<>());
            if(field.isAnnotationPresent(Join.class)){
                Join join = field.getAnnotation(Join.class);
                javax.persistence.criteria.Join jpaJoin = joinMap.getOrDefault(join.joinObject(),root.join(join.joinObject(), JoinType.LEFT));
                setPredicate(root, predicates1, field, fieldName, fieldObject, cb, jpaJoin);
                joinMap.put(join.joinObject(),jpaJoin);
            }else {
                setPredicate(root, predicates1, field, fieldName, fieldObject, cb, null);
            }
            orPredicateMap.put(groupOr.value(), predicates1);
        }else if(field.isAnnotationPresent(GroupAnd.class)){
            GroupAnd groupAnd = field.getAnnotation(GroupAnd.class);
            List<Predicate> predicates1 = andPredicateMap.getOrDefault(groupAnd.value(), new ArrayList<>());
            if(field.isAnnotationPresent(Join.class)){
                Join join = field.getAnnotation(Join.class);
                javax.persistence.criteria.Join jpaJoin = joinMap.getOrDefault(join.joinObject(),root.join(join.joinObject(), JoinType.LEFT));
                setPredicate(root, predicates1, field, fieldName, fieldObject, cb, jpaJoin);
                joinMap.put(join.joinObject(),jpaJoin);
            }else {
                setPredicate(root, predicates1, field, fieldName, fieldObject, cb, null);
            }
            andPredicateMap.put(groupAnd.value(), predicates1);
        } else {
            if(field.isAnnotationPresent(Join.class)){
                Join join = field.getAnnotation(Join.class);
                javax.persistence.criteria.Join jpaJoin = joinMap.getOrDefault(join.joinObject(),root.join(join.joinObject(), JoinType.LEFT));
                setPredicate(root, predicates, field, fieldName, fieldObject, cb, jpaJoin);
                joinMap.put(join.joinObject(),jpaJoin);
            }else {
                setPredicate(root, predicates, field, fieldName, fieldObject, cb, null);
            }
        }
    }

    private static void setPredicate(Root root, List<Predicate> predicates, Field field, String fieldName, Object fieldObject, CriteriaBuilder cb, javax.persistence.criteria.Join jpaJoin) {
        //字段如果为集合类型，做In操作
        if (Collection.class.isAssignableFrom(field.getType())) {

            if (field.isAnnotationPresent(Join.class)){
                Join join = field.getAnnotation(Join.class);
                if (field.isAnnotationPresent(Ne.class)){
                    predicates.add(jpaJoin.get(join.joinObjectAttributeName()).in(fieldObject).not());
                }else {
                    predicates.add(jpaJoin.get(join.joinObjectAttributeName()).in(fieldObject));
                }
            }else {
                if (field.isAnnotationPresent(Ne.class)){
                    predicates.add(root.get(fieldName).in(fieldObject).not());
                }else {
                    predicates.add(root.get(fieldName).in(fieldObject));
                }
            }
        } else {
            if (field.isAnnotationPresent(Like.class)) {
                if (field.isAnnotationPresent(Join.class)){
                    likeHandlerJoin(root, predicates, field, fieldName, fieldObject, cb, jpaJoin);
                }else {
                    likeHandler(root, predicates, field, fieldName, fieldObject, cb);
                }
            } else if (field.isAnnotationPresent(GreaterThan.class)) {
                if (field.isAnnotationPresent(Join.class)){
                    Join join = field.getAnnotation(Join.class);
                    predicates.add(cb.greaterThan(jpaJoin.get(join.joinObjectAttributeName()),(Comparable) fieldObject));
                }else {
                    predicates.add(cb.greaterThan(root.get(fieldName), (Comparable) fieldObject));
                }
            } else if (field.isAnnotationPresent(GreaterThanEqual.class)) {
                if (field.isAnnotationPresent(Join.class)){
                    Join join = field.getAnnotation(Join.class);
                    predicates.add(cb.greaterThanOrEqualTo(jpaJoin.get(join.joinObjectAttributeName()),(Comparable) fieldObject));
                }else {
                    predicates.add(cb.greaterThanOrEqualTo(root.get(fieldName), (Comparable) fieldObject));
                }
            } else if (field.isAnnotationPresent(LessThan.class)) {
                if (field.isAnnotationPresent(Join.class)){
                    Join join = field.getAnnotation(Join.class);
                    predicates.add(cb.lessThan(jpaJoin.get(join.joinObjectAttributeName()),(Comparable) fieldObject));
                }else {
                    predicates.add(cb.lessThan(root.get(fieldName), (Comparable) fieldObject));
                }
            } else if (field.isAnnotationPresent(LessThanEqual.class)) {
                if (field.isAnnotationPresent(Join.class)){
                    Join join = field.getAnnotation(Join.class);
                    predicates.add(cb.lessThanOrEqualTo(jpaJoin.get(join.joinObjectAttributeName()),(Comparable) fieldObject));
                }else {
                    predicates.add(cb.lessThanOrEqualTo(root.get(fieldName), (Comparable) fieldObject));
                }
            } else if (field.isAnnotationPresent(SmartLike.class)) {
                smartLikeHandler(root, predicates, fieldName, fieldObject, cb);
            } else if (field.isAnnotationPresent(NonNull.class)) {
                if (field.isAnnotationPresent(Join.class)){
                    Join join = field.getAnnotation(Join.class);
                    predicates.add(cb.isNotNull(jpaJoin.get(join.joinObjectAttributeName())));
                }else {
                    predicates.add(cb.isNotNull(root.get(fieldName)));
                }
            } else if (field.isAnnotationPresent(IsNull.class)) {
                if (field.isAnnotationPresent(Join.class)){
                    Join join = field.getAnnotation(Join.class);
                    predicates.add(cb.isNull(jpaJoin.get(join.joinObjectAttributeName())));
                }else {
                    predicates.add(cb.isNull(root.get(fieldName)));
                }
            } else if (field.isAnnotationPresent(Ne.class)) {
                Ne ne = field.getAnnotation(Ne.class);
                if (field.isAnnotationPresent(Join.class)){
                    Join join = field.getAnnotation(Join.class);
                    predicates.add(cb.notEqual(jpaJoin.get(join.joinObjectAttributeName()), fieldObject));
                }else {
                    predicates.add(cb.notEqual(root.get(ne.fieldName()), fieldObject));
                }
            } else if (field.isAnnotationPresent(GreaterThanField.class)) {
                GreaterThanField greaterThanField = field.getAnnotation(GreaterThanField.class);
                if (field.isAnnotationPresent(Join.class)){
                    Join join = field.getAnnotation(Join.class);
                    predicates.add(cb.greaterThan(jpaJoin.get(join.joinObjectAttributeName()),(Comparable) fieldObject));
                }else {
                    predicates.add(cb.greaterThan(root.get(greaterThanField.fieldName()), (Comparable) fieldObject));
                }
            } else if (field.isAnnotationPresent(GreaterThanEqualField.class)) {
                GreaterThanEqualField greaterThanEqualField = field.getAnnotation(GreaterThanEqualField.class);
                if (field.isAnnotationPresent(Join.class)){
                    Join join = field.getAnnotation(Join.class);
                    predicates.add(cb.greaterThanOrEqualTo(jpaJoin.get(join.joinObjectAttributeName()),(Comparable) fieldObject));
                }else {
                    predicates.add(cb.greaterThanOrEqualTo(root.get(greaterThanEqualField.fieldName()), (Comparable) fieldObject));
                }
            } else if (field.isAnnotationPresent(LessThanField.class)) {
                LessThanField lessThanField = field.getAnnotation(LessThanField.class);
                if (field.isAnnotationPresent(Join.class)){
                    Join join = field.getAnnotation(Join.class);
                    predicates.add(cb.lessThan(jpaJoin.get(join.joinObjectAttributeName()),(Comparable) fieldObject));
                }else {
                    predicates.add(cb.lessThan(root.get(lessThanField.fieldName()), (Comparable) fieldObject));
                }
            } else if (field.isAnnotationPresent(LessThanEqualField.class)) {
                LessThanEqualField lessThanEqualField = field.getAnnotation(LessThanEqualField.class);
                if (field.isAnnotationPresent(Join.class)){
                    Join join = field.getAnnotation(Join.class);
                    predicates.add(cb.lessThanOrEqualTo(jpaJoin.get(join.joinObjectAttributeName()),(Comparable) fieldObject));
                }else {
                    predicates.add(cb.lessThanOrEqualTo(root.get(lessThanEqualField.fieldName()), (Comparable) fieldObject));
                }
            } else {
                if (field.isAnnotationPresent(Join.class)){
                    Join join = field.getAnnotation(Join.class);
                    predicates.add(cb.equal(jpaJoin.get(join.joinObjectAttributeName()), fieldObject));
                }else {
                    predicates.add(cb.equal(root.get(fieldName), fieldObject));
                }
            }
        }
    }

    private static void smartLikeHandler(Root root, List<Predicate> predicates, String fieldName, Object fieldObject, CriteriaBuilder cb) {
        if (fieldObject.toString().contains(PERCENT)) {
            predicates.add(cb.like(root.get(fieldName), fieldObject.toString()));
        } else {
            predicates.add(cb.equal(root.get(fieldName), fieldObject));
        }
    }

    private static void likeHandler(Root root, List<Predicate> predicates, Field field, String fieldName, Object fieldObject, CriteriaBuilder cb) {
        Like like = field.getAnnotation(Like.class);
        String location = like.location();
        if (Like.AROUND.equals(location)) {
            predicates.add(cb.like(root.get(fieldName), PERCENT + fieldObject + PERCENT));
        }
        if (Like.LEFT.equals(location)) {
            predicates.add(cb.like(root.get(fieldName), PERCENT + fieldObject));
        }
        if (Like.RIGHT.equals(location)) {
            predicates.add(cb.like(root.get(fieldName), fieldObject + PERCENT));
        }
    }

    private static void likeHandlerJoin(Root root, List<Predicate> predicates, Field field, String fieldName, Object fieldObject, CriteriaBuilder cb, javax.persistence.criteria.Join jpaJoin) {
        Like like = field.getAnnotation(Like.class);
        Join join = field.getAnnotation(Join.class);
        String location = like.location();
        if (Like.AROUND.equals(location)) {
            predicates.add(cb.like(jpaJoin.get(join.joinObjectAttributeName()),PERCENT + fieldObject + PERCENT));
        }
        if (Like.LEFT.equals(location)) {
            predicates.add(cb.like(jpaJoin.get(join.joinObjectAttributeName()),PERCENT + fieldObject));
        }
        if (Like.RIGHT.equals(location)) {
            predicates.add(cb.like(jpaJoin.get(join.joinObjectAttributeName()),fieldObject + PERCENT));
        }
    }
}

